package zm.irc.ioc;

import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;

import org.apache.commons.collections.CollectionUtils;
import org.apache.log4j.Logger;

import zm.irc.ioc.annotation.AutoWired;
import zm.irc.ioc.annotation.Value;
import zm.irc.ioc.beanActoin.AfterContexinItialize;
import zm.irc.ioc.beanActoin.BeanPost;
import zm.irc.ioc.classCollector.ClassCollector;
import zm.irc.ioc.pojo.ConfigurationCache;
import zm.irc.ioc.pojo.WrapBeanObj;
import zm.irc.ioc.pojo.WrapClass;
import zm.irc.ioc.pojo.WrapField;

public class ApplicationInitalizion {
    private static final Logger log = Logger.getLogger(ApplicationInitalizion.class);

    private static final ApplicationInitalizion appInit = new ApplicationInitalizion();
  
    public static ApplicationContext initApplicationContext(){
      //  URL xmlpath = 
        String rootPath = System.getProperty("user.dir") + "/target/classes/";
        log.error("To be refactor");
        log.info("Application init start ! Root path is " + rootPath);
        return appInit.initApplicationContext(rootPath);
    }

    
    ApplicationContext initApplicationContext(String rootPath){
        ApplicationContext context = new ApplicationContext();

        WrapBeanObj wrapContext = new WrapBeanObj(WrapClass.build(ApplicationContext.class),context );
        context.putBean(wrapContext);
        
        Set<WrapClass> beanClasses = ClassCollector.CLASS_COLLECTOR.collecte(rootPath);
    
        /* Collect all of bean values (Both of Singlet and Multi) */
        Map<String,WrapBeanObj> tempBeanPool = new HashMap<>();
        tempBeanPool.put(wrapContext.getWrapClass().getBeanId(), wrapContext);
       
        for(WrapClass wrapClass : beanClasses){
            context.addIntoCache(wrapClass);
            if(!wrapClass.isSinglet()){
                continue;
            }
            
            /* Check if the singlet bean have been created!
                 Maybe it already be created by some other class by autowared-field's initialization.
            */
            if( context.getBeanById(wrapClass.getBeanId()) != null){
                continue; 
            }
            
           this.buildBean(wrapClass, context,tempBeanPool,null);
          
        }
        this.afterContexinItialize(tempBeanPool, context);
        return context;
    } 


    private void afterContexinItialize(Map<String,WrapBeanObj> tempBeanPool,ApplicationContext context){
    
        this.doAfterContexinItialize(tempBeanPool);
        this.doAfterContexinItialize(context.getAllBeans());

    }

    private void doAfterContexinItialize(Map<String,WrapBeanObj> beas ){
        for(Entry<String,WrapBeanObj> entry: beas.entrySet()){
            WrapBeanObj bean = entry.getValue();
            Object beanObj = bean.getBeanObj();
            if(beanObj instanceof AfterContexinItialize){
                AfterContexinItialize ac = (AfterContexinItialize) beanObj;
                ac.doAction();
            }
        }
    }


    WrapBeanObj buildBean(WrapClass wrapClass,ApplicationContext context
                        ,Map<String,WrapBeanObj> tempBeanPool
                        ,Object parentBean){

       

        WrapBeanObj bean = this.createBean(wrapClass);
        if(bean == null){
            return null;
        }
        this.putBean(bean, context,tempBeanPool,parentBean);
        this.enrichBean(bean,context,tempBeanPool,parentBean);
        Object beanObject = bean.getBeanObj();
        if( beanObject instanceof BeanPost){
            beanObject = ((BeanPost)beanObject).post(beanObject);
            bean.setBeanObject(beanObject);
        }
        return bean;
    }

    /**
     * <pre>
     * Put bean into :
     *  1. for all : tempBeanPool
     *  2. ApplicationContext(Only for Singlet Bean)
     * </pre>
     * @param bean
     * @param context
     * @param tempBeanPool
     */
    private void putBean(WrapBeanObj bean,ApplicationContext context
                        , Map<String,WrapBeanObj> tempBeanPool
                        ,Object parentBean){
        if(bean == null){
            return;
        }
      
        if(bean.getWrapClass().isSinglet()){
            context.putBean(bean);
        }
        
        tempBeanPool.put(this.buildBeanIdForTempPool(bean.getWrapClass(),parentBean), bean);
    }

    /**
     * Priority :
     *  1. {@link this#tempBeanPool}
     *  2. ApplicationContext
     * @return
     */
    private WrapBeanObj getBean(WrapClass wrapClass, 
                                ApplicationContext context, 
                                Map<String,WrapBeanObj> tempBeanPool,
                                Object parentBean){
        if(wrapClass == null || context == null){
            return null;
        }

        String beanIdInTempPool = this.buildBeanIdForTempPool(wrapClass,parentBean);
        WrapBeanObj bean = tempBeanPool.get(beanIdInTempPool);
        
        if( bean == null){
            Object obj = context.getBean(wrapClass.getOriginClass());
            if(obj != null){
                bean =  new WrapBeanObj(wrapClass,obj);
            }
        }
        return bean;

    }

    private String buildBeanIdForTempPool(WrapClass wrapClass,Object parent){
        if(wrapClass == null){
            return null;
        }
        String beanId = wrapClass.getBeanId();
        if(parent == null){
            return beanId;
        }
        return  parent.toString() + "." + beanId;
        
    }
    /**
     * <pre>
     * Build bean (Recursively)
     *   All of fields which annotation by {@link AutoWired} are will be created.
     *   only Singlet bean (field value) will be added into ApplicationContext.
     * </pre>
     * @param bean
     * @param context
     * @return
     */
    private void enrichBean(WrapBeanObj bean,ApplicationContext context,
                            Map<String,WrapBeanObj> tempBeanPool,
                            Object parentBean){
       WrapClass wrapClass = bean.getWrapClass();
       if(wrapClass == null){
           return;
       }
       List<WrapField> autoWiredFields = wrapClass.getAutoWiredFields();
       if(CollectionUtils.isEmpty(autoWiredFields)){
           return;
       }

       for(WrapField autoWiredField : autoWiredFields){
            Annotation iocAnoo = autoWiredField.getIocAnno();
            if(iocAnoo == null){
                continue;
            }
            if(iocAnoo instanceof AutoWired){
                this.autoWired(bean,autoWiredField,context,tempBeanPool);
            }else if(iocAnoo instanceof Value){
                this.valueAnno(bean,autoWiredField);
            }else{
                throw new RuntimeException("Unsupport annotation: " + iocAnoo);
            }
       }

    }

    private void valueAnno(WrapBeanObj bean, WrapField field){
        String cfgFileName = bean.getWrapClass().getCfgFileName();
        String valuePath = ((Value)field.getIocAnno()).path();
        Object fieldaValue = ConfigurationCache.getValue(cfgFileName, valuePath);
       
        this.setFieldValue(bean, field, fieldaValue);
    }


    private void autoWired(WrapBeanObj bean, WrapField field,ApplicationContext context, Map<String,WrapBeanObj> tempBeanPool){
        WrapBeanObj fieldValue = this.getBean(field.getFieldClass(),context,tempBeanPool,bean);
        if(fieldValue == null){
            fieldValue = this.buildBean(field.getFieldClass(),context,tempBeanPool,bean);
        }
        if(fieldValue == null){
            throw new RuntimeException("AutoWired error");
        }
        this.setFieldValue(bean, field, fieldValue.getBeanObj());

    }

    private void setFieldValue(WrapBeanObj obj,WrapField field,Object fieldValue){
        Field originField = field.getOriginField();
        try{
         //   boolean accessible = originField.canAccess(null);
            originField.setAccessible(true);
            originField.set(obj.getBeanObj(), fieldValue);
            originField.setAccessible(false);
        }catch(Exception e){
            log.error("enrichment error!",e);
        }
    }

     /**
     * Create bean object by reflect.(without any enrichment!)
     * @param warpClass
     * @return
     */
    private WrapBeanObj createBean(WrapClass warpClass){
        if(warpClass == null || warpClass.getOriginClass() == null){
            return null;
        }
        try{
            Class<?> clazz = warpClass.getOriginClass();
            Object o = clazz.getConstructor().newInstance();
            WrapBeanObj beanObj = new WrapBeanObj(warpClass,o);
            return beanObj;
        }catch(Exception e){
            log.error("Init bean error.",e);
        }
        return null;
    }
 
}
